import { ConfigurationChangeEvent, ConfigurationTarget, Disposable, EventEmitter, Uri, WorkspaceConfiguration } from "vscode";
import { IInterpreterAutoSelectionProxyService } from "../interpreter/autoSelection/types";
import * as path from 'path';
import * as fs from 'fs';
import './extension';
import { sendTelemetryEvent } from "../telemetry";
import { EventName } from "../telemetry/constants";
import { IWorkspaceService } from "./application/types";
import { WorkspaceService } from "./application/workspace";
import { IDefaultLanguageServer, IEQSettings, IFormattingSettings, IInterpreterPathService, IS_WINDOWS, ITerminalSettings, Resource } from "./types";
import { debounceSync } from "./utils/decorators";
import { SystemVariables } from "./variables/systemVariables";
import { DEFAULT_INTERPRETER_SETTING, isTestExecution } from "./constants";
import { LanguageServerType } from "../activation/types";
import { getOSType, OSType } from "./utils/platform";

const untildify = require('untildify');

export class EQSettings implements IEQSettings {

    private _defaultInterpreterPath = '';

    private _eqPath = 'eq';

    private readonly workspace: IWorkspaceService;

    private workspaceRoot: Resource;

    private static eqSettings: Map<string, EQSettings> = new Map<string, EQSettings>();

    protected readonly changed = new EventEmitter<void>();

    private disposables: Disposable[] = [];

    public formatting!: IFormattingSettings;

    constructor(
        workspaceFolder: Resource,
        private readonly interpreterAutoSelectionService: IInterpreterAutoSelectionProxyService,
        workspace: IWorkspaceService,
        private readonly interpreterPathService: IInterpreterPathService,
        private readonly defaultLS: IDefaultLanguageServer | undefined,
    ) {
        this.workspace = workspace || new WorkspaceService();
        this.workspaceRoot = workspaceFolder;

        this.initialize();
    }
    public terminal!: ITerminalSettings;

    public static getInstance(
        resource: Uri | undefined,
        interpreterAutoSelectionService: IInterpreterAutoSelectionProxyService,
        workspace: IWorkspaceService,
        interpreterPathService: IInterpreterPathService,
        defaultLS: IDefaultLanguageServer | undefined,
    ): EQSettings {
        workspace = workspace || new WorkspaceService();
        const workspaceFolderUri = EQSettings.getSettingsUriAndTarget(resource, workspace).uri;
        const workspaceFolderKey = workspaceFolderUri ? workspaceFolderUri.fsPath : '';

        if (!EQSettings.eqSettings.has(workspaceFolderKey)) {
            const settings = new EQSettings(
                workspaceFolderUri,
                interpreterAutoSelectionService,
                workspace,
                interpreterPathService,
                defaultLS,
            );
            EQSettings.eqSettings.set(workspaceFolderKey, settings);
            const config = workspace.getConfiguration('editor', resource || (null as any));
            const formatOnType = config ? config.get('formatOnType', false) : false;
            sendTelemetryEvent(EventName.FORMAT_ON_TYPE, undefined, { enabled: formatOnType });
        }

        return EQSettings.eqSettings.get(workspaceFolderKey)!;
    }

    public get defaultInterpreterPath(): string {
        return this._defaultInterpreterPath;
    }

    public set defaultInterpreterPath(value: string) {
        if (this._defaultInterpreterPath === value) {
            return;
        }
        // Add support for specifying just the directory where the python executable will be located.
        // E.g. virtual directory name.
        try {
            this._defaultInterpreterPath = this.getPythonExecutable(value);
        } catch (ex) {
            this._defaultInterpreterPath = value;
        }
    }

    public initialize(): void {
        const onDidChange = () => {
            const currentConfig = this.workspace.getConfiguration('eq', this.workspaceRoot);
            this.update(currentConfig);

            // If workspace config changes, then we could have a cascading effect of on change events.
            // Let's defer the change notification.
            this.debounceChangeNotification();
        };
        this.disposables.push(this.workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this));
        this.disposables.push(
            this.interpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(onDidChange.bind(this)),
        );
        this.disposables.push(
            this.workspace.onDidChangeConfiguration((event: ConfigurationChangeEvent) => {
                if (event.affectsConfiguration('eq')) {
                    onDidChange();
                }
            }),
        );
        if (this.interpreterPathService) {
            this.disposables.push(this.interpreterPathService.onDidChange(onDidChange.bind(this)));
        }

        const initialConfig = this.workspace.getConfiguration('eq', this.workspaceRoot);
        if (initialConfig) {
            this.update(initialConfig);
        }
    }

    protected onWorkspaceFoldersChanged(): void {
        // If an activated workspace folder was removed, delete its key
        const workspaceKeys = this.workspace.workspaceFolders!.map((workspaceFolder) => workspaceFolder.uri.fsPath);
        const activatedWkspcKeys = Array.from(EQSettings.eqSettings.keys());
        const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter((item) => workspaceKeys.indexOf(item) < 0);
        if (activatedWkspcFoldersRemoved.length > 0) {
            for (const folder of activatedWkspcFoldersRemoved) {
                EQSettings.eqSettings.delete(folder);
            }
        }
    }

    public static getSettingsUriAndTarget(
        resource: Uri | undefined,
        workspace?: IWorkspaceService,
    ): { uri: Uri | undefined; target: ConfigurationTarget } {
        workspace = workspace || new WorkspaceService();
        const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined;
        let workspaceFolderUri: Uri | undefined = workspaceFolder ? workspaceFolder.uri : undefined;

        if (!workspaceFolderUri && Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) {
            workspaceFolderUri = workspace.workspaceFolders[0].uri;
        }

        const target = workspaceFolderUri ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Global;
        return { uri: workspaceFolderUri, target };
    }

    @debounceSync(1)
    protected debounceChangeNotification(): void {
        this.changed.fire();
    }
    public dispose(): void {
        this.disposables.forEach((disposable) => disposable && disposable.dispose());
        this.disposables = [];
    }

    public get eqPath(): string {
        return this._eqPath;
    }

    public set eqPath(value: string) {
        if (this._eqPath === value) {
            return;
        }
        // Add support for specifying just the directory where the eq executable will be located.
        // E.g. virtual directory name.
        try {
            this._eqPath = this.getPythonExecutable(value);
        } catch (ex) {
            this._eqPath = value;
        }
    }

    protected getPythonExecutable(eqPath: string): string {
        return getPythonExecutable(eqPath);
    }

    private getPythonPath(systemVariables: SystemVariables, workspaceRoot: string | undefined) {
        this.eqPath = systemVariables.resolveAny(this.interpreterPathService.get(this.workspaceRoot))!;
        if (
            !process.env.CI_DISABLE_AUTO_SELECTION &&
            (this.eqPath.length === 0 || this.eqPath === 'eq') &&
            this.interpreterAutoSelectionService
        ) {
            const autoSelectedPythonInterpreter = this.interpreterAutoSelectionService.getAutoSelectedInterpreter(
                this.workspaceRoot,
            );
            if (autoSelectedPythonInterpreter) {
                this.eqPath = autoSelectedPythonInterpreter.path;
                if (this.workspaceRoot) {
                    this.interpreterAutoSelectionService
                        .setWorkspaceInterpreter(this.workspaceRoot, autoSelectedPythonInterpreter)
                        .ignoreErrors();
                }
            }
        }
        return getAbsolutePath(this.eqPath, workspaceRoot);
    }
    
    protected update(eqSettings: WorkspaceConfiguration): void {
        console.log("EQSettings update");
        const workspaceRoot = this.workspaceRoot?.fsPath;
        const systemVariables: SystemVariables = new SystemVariables(undefined, workspaceRoot, this.workspace);

        this.eqPath = this.getPythonPath(systemVariables, workspaceRoot);

        const defaultInterpreterPath = systemVariables.resolveAny(eqSettings.get<string>('defaultInterpreterPath'));
        this.defaultInterpreterPath = defaultInterpreterPath || DEFAULT_INTERPRETER_SETTING;
        if (this.defaultInterpreterPath === DEFAULT_INTERPRETER_SETTING) {
            const autoSelectedPythonInterpreter = this.interpreterAutoSelectionService.getAutoSelectedInterpreter(
                this.workspaceRoot,
            );
            this.defaultInterpreterPath = autoSelectedPythonInterpreter?.path ?? this.defaultInterpreterPath;
        }
        this.defaultInterpreterPath = getAbsolutePath(this.defaultInterpreterPath, workspaceRoot);

        // this.venvPath = systemVariables.resolveAny(eqSettings.get<string>('venvPath'))!;
        // this.venvFolders = systemVariables.resolveAny(eqSettings.get<string[]>('venvFolders'))!;
        // const condaPath = systemVariables.resolveAny(eqSettings.get<string>('condaPath'))!;
        // this.condaPath = condaPath && condaPath.length > 0 ? getAbsolutePath(condaPath, workspaceRoot) : condaPath;
        // const pipenvPath = systemVariables.resolveAny(eqSettings.get<string>('pipenvPath'))!;
        // this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath;
        // const poetryPath = systemVariables.resolveAny(eqSettings.get<string>('poetryPath'))!;
        // this.poetryPath = poetryPath && poetryPath.length > 0 ? getAbsolutePath(poetryPath, workspaceRoot) : poetryPath;

        // Get as a string and verify; don't just accept.
        let userLS = eqSettings.get<string>('languageServer');
        userLS = systemVariables.resolveAny(userLS);

        // Validate the user's input; if invalid, set it to the default.
        // if (
        //     !userLS ||
        //     userLS === 'Default' ||
        //     userLS === 'Microsoft' ||
        //     !Object.values(LanguageServerType).includes(userLS as LanguageServerType)
        // ) {
        //     this.languageServer = this.defaultLS?.defaultLSType ?? LanguageServerType.None;
        //     this.languageServerIsDefault = true;
        // } else if (userLS === 'JediLSP') {
        //     // Switch JediLSP option to Jedi.
        //     this.languageServer = LanguageServerType.Jedi;
        //     this.languageServerIsDefault = false;
        // } else {
        //     this.languageServer = userLS as LanguageServerType;
        //     this.languageServerIsDefault = false;
        // }

        // const autoCompleteSettings = systemVariables.resolveAny(
        //     eqSettings.get<IAutoCompleteSettings>('autoComplete'),
        // )!;
        // if (this.autoComplete) {
        //     Object.assign<IAutoCompleteSettings, IAutoCompleteSettings>(this.autoComplete, autoCompleteSettings);
        // } else {
        //     this.autoComplete = autoCompleteSettings;
        // }

        // const envFileSetting = eqSettings.get<string>('envFile');
        // this.envFile = systemVariables.resolveAny(envFileSetting)!;
        // sendSettingTelemetry(this.workspace, envFileSetting);

        // // eslint-disable-next-line @typescript-eslint/no-explicit-any
        // this.devOptions = systemVariables.resolveAny(eqSettings.get<any[]>('devOptions'))!;
        // this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : [];

        // const lintingSettings = systemVariables.resolveAny(eqSettings.get<ILintingSettings>('linting'))!;
        // if (this.linting) {
        //     Object.assign<ILintingSettings, ILintingSettings>(this.linting, lintingSettings);
        // } else {
        //     this.linting = lintingSettings;
        // }

        // this.disableInstallationChecks = eqSettings.get<boolean>('disableInstallationCheck') === true;
        // this.globalModuleInstallation = eqSettings.get<boolean>('globalModuleInstallation') === true;

        // const sortImportSettings = systemVariables.resolveAny(eqSettings.get<ISortImportSettings>('sortImports'))!;
        // if (this.sortImports) {
        //     Object.assign<ISortImportSettings, ISortImportSettings>(this.sortImports, sortImportSettings);
        // } else {
        //     this.sortImports = sortImportSettings;
        // }
        // // Support for travis.
        // this.sortImports = this.sortImports ? this.sortImports : { path: '', args: [] };
        // // Support for travis.
        // this.linting = this.linting
        //     ? this.linting
        //     : {
        //         enabled: false,
        //         cwd: undefined,
        //         ignorePatterns: [],
        //         flake8Args: [],
        //         flake8Enabled: false,
        //         flake8Path: 'flake8',
        //         lintOnSave: false,
        //         maxNumberOfProblems: 100,
        //         mypyArgs: [],
        //         mypyEnabled: false,
        //         mypyPath: 'mypy',
        //         banditArgs: [],
        //         banditEnabled: false,
        //         banditPath: 'bandit',
        //         pycodestyleArgs: [],
        //         pycodestyleEnabled: false,
        //         pycodestylePath: 'pycodestyle',
        //         pylamaArgs: [],
        //         pylamaEnabled: false,
        //         pylamaPath: 'pylama',
        //         prospectorArgs: [],
        //         prospectorEnabled: false,
        //         prospectorPath: 'prospector',
        //         pydocstyleArgs: [],
        //         pydocstyleEnabled: false,
        //         pydocstylePath: 'pydocstyle',
        //         pylintArgs: [],
        //         pylintEnabled: false,
        //         pylintPath: 'pylint',
        //         pylintCategorySeverity: {
        //             convention: DiagnosticSeverity.Hint,
        //             error: DiagnosticSeverity.Error,
        //             fatal: DiagnosticSeverity.Error,
        //             refactor: DiagnosticSeverity.Hint,
        //             warning: DiagnosticSeverity.Warning,
        //         },
        //         pycodestyleCategorySeverity: {
        //             E: DiagnosticSeverity.Error,
        //             W: DiagnosticSeverity.Warning,
        //         },
        //         flake8CategorySeverity: {
        //             E: DiagnosticSeverity.Error,
        //             W: DiagnosticSeverity.Warning,
        //             // Per http://flake8.pycqa.org/en/latest/glossary.html#term-error-code
        //             // 'F' does not mean 'fatal as in PyLint but rather 'pyflakes' such as
        //             // unused imports, variables, etc.
        //             F: DiagnosticSeverity.Warning,
        //         },
        //         mypyCategorySeverity: {
        //             error: DiagnosticSeverity.Error,
        //             note: DiagnosticSeverity.Hint,
        //         },
        //     };
        // this.linting.pylintPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylintPath), workspaceRoot);
        // this.linting.flake8Path = getAbsolutePath(systemVariables.resolveAny(this.linting.flake8Path), workspaceRoot);
        // this.linting.pycodestylePath = getAbsolutePath(
        //     systemVariables.resolveAny(this.linting.pycodestylePath),
        //     workspaceRoot,
        // );
        // this.linting.pylamaPath = getAbsolutePath(systemVariables.resolveAny(this.linting.pylamaPath), workspaceRoot);
        // this.linting.prospectorPath = getAbsolutePath(
        //     systemVariables.resolveAny(this.linting.prospectorPath),
        //     workspaceRoot,
        // );
        // this.linting.pydocstylePath = getAbsolutePath(
        //     systemVariables.resolveAny(this.linting.pydocstylePath),
        //     workspaceRoot,
        // );
        // this.linting.mypyPath = getAbsolutePath(systemVariables.resolveAny(this.linting.mypyPath), workspaceRoot);
        // this.linting.banditPath = getAbsolutePath(systemVariables.resolveAny(this.linting.banditPath), workspaceRoot);

        // if (this.linting.cwd) {
        //     this.linting.cwd = getAbsolutePath(systemVariables.resolveAny(this.linting.cwd), workspaceRoot);
        // }

        const formattingSettings = systemVariables.resolveAny(eqSettings.get<IFormattingSettings>('formatting'))!;
        if (this.formatting) {
            Object.assign<IFormattingSettings, IFormattingSettings>(this.formatting, formattingSettings);
        } else {
            this.formatting = formattingSettings;
        }
        // Support for travis.
        this.formatting = this.formatting
            ? this.formatting
            : {
                autopep8Args: [],
                autopep8Path: 'autopep8',
                provider: 'autopep8',
                blackArgs: [],
                blackPath: 'black',
                yapfArgs: [],
                yapfPath: 'yapf',
            };
        this.formatting.autopep8Path = getAbsolutePath(
            systemVariables.resolveAny(this.formatting.autopep8Path),
            workspaceRoot,
        );
        this.formatting.yapfPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.yapfPath), workspaceRoot);
        this.formatting.blackPath = getAbsolutePath(
            systemVariables.resolveAny(this.formatting.blackPath),
            workspaceRoot,
        );

        // const testSettings = systemVariables.resolveAny(eqSettings.get<ITestingSettings>('testing'))!;
        // if (this.testing) {
        //     Object.assign<ITestingSettings, ITestingSettings>(this.testing, testSettings);
        // } else {
        //     this.testing = testSettings;
        //     if (isTestExecution() && !this.testing) {
        //         this.testing = {
        //             pytestArgs: [],
        //             unittestArgs: [],
        //             promptToConfigure: true,
        //             debugPort: 3000,
        //             pytestEnabled: false,
        //             unittestEnabled: false,
        //             pytestPath: 'pytest',
        //             autoTestDiscoverOnSaveEnabled: true,
        //         } as ITestingSettings;
        //     }
        // }

        // // Support for travis.
        // this.testing = this.testing
        //     ? this.testing
        //     : {
        //         promptToConfigure: true,
        //         debugPort: 3000,
        //         pytestArgs: [],
        //         pytestEnabled: false,
        //         pytestPath: 'pytest',
        //         unittestArgs: [],
        //         unittestEnabled: false,
        //         autoTestDiscoverOnSaveEnabled: true,
        //     };
        // this.testing.pytestPath = getAbsolutePath(systemVariables.resolveAny(this.testing.pytestPath), workspaceRoot);
        // if (this.testing.cwd) {
        //     this.testing.cwd = getAbsolutePath(systemVariables.resolveAny(this.testing.cwd), workspaceRoot);
        // }

        // // Resolve any variables found in the test arguments.
        // this.testing.pytestArgs = this.testing.pytestArgs.map((arg) => systemVariables.resolveAny(arg));
        // this.testing.unittestArgs = this.testing.unittestArgs.map((arg) => systemVariables.resolveAny(arg));

        const terminalSettings = systemVariables.resolveAny(eqSettings.get<ITerminalSettings>('terminal'))!;
        if (this.terminal) {
            Object.assign<ITerminalSettings, ITerminalSettings>(this.terminal, terminalSettings);
        } else {
            this.terminal = terminalSettings;
            if (isTestExecution() && !this.terminal) {
                this.terminal = {} as ITerminalSettings;
            }
        }
        // Support for travis.
        this.terminal = this.terminal
            ? this.terminal
            : {
                executeInFileDir: true,
                focusAfterLaunch: false,
                launchArgs: [],
                activateEnvironment: true,
                activateEnvInCurrentTerminal: false,
            };

        // const experiments = systemVariables.resolveAny(eqSettings.get<IExperiments>('experiments'))!;
        // if (this.experiments) {
        //     Object.assign<IExperiments, IExperiments>(this.experiments, experiments);
        // } else {
        //     this.experiments = experiments;
        // }
        // // Note we directly access experiment settings using workspace service in ExperimentService class.
        // // Any changes here specific to these settings should propogate their as well.
        // this.experiments = this.experiments
        //     ? this.experiments
        //     : {
        //         enabled: true,
        //         optInto: [],
        //         optOutFrom: [],
        //     };

        // this.tensorBoard = eqSettings.get<ITensorBoardSettings>('tensorBoard');
        console.log("EQSettings update");
    }
}

function getAbsolutePath(pathToCheck: string, rootDir: string | undefined): string {
    if (!rootDir) {
        rootDir = __dirname;
    }

    pathToCheck = untildify(pathToCheck) as string;
    if (isTestExecution() && !pathToCheck) {
        return rootDir;
    }
    if (pathToCheck.indexOf(path.sep) === -1) {
        return pathToCheck;
    }
    return path.isAbsolute(pathToCheck) ? pathToCheck : path.resolve(rootDir, pathToCheck);
}

function getPythonExecutable(eqPath: string): string {
    eqPath = untildify(eqPath) as string;

    // If only 'eq'.
    if (
        eqPath === 'eq' ||
        eqPath.indexOf(path.sep) === -1 ||
        path.basename(eqPath) === path.dirname(eqPath)
    ) {
        return eqPath;
    }

    if (isValidPythonPath(eqPath)) {
        return eqPath;
    }
    // Keep eq right on top, for backwards compatibility.

    const KnownPythonExecutables = [
        'eq',
        'eq4',
        'eq3.6',
        'eq3.5',
        'eq3',
        'eq2.7',
        'eq2',
        'eq3.7',
        'eq3.8',
        'eq3.9',
    ];

    for (let executableName of KnownPythonExecutables) {
        // Suffix with 'eq' for linux and 'osx', and 'eq.exe' for 'windows'.
        if (IS_WINDOWS) {
            executableName = `${executableName}.exe`;
            if (isValidPythonPath(path.join(eqPath, executableName))) {
                return path.join(eqPath, executableName);
            }
            if (isValidPythonPath(path.join(eqPath, 'Scripts', executableName))) {
                return path.join(eqPath, 'Scripts', executableName);
            }
        } else {
            if (isValidPythonPath(path.join(eqPath, executableName))) {
                return path.join(eqPath, executableName);
            }
            if (isValidPythonPath(path.join(eqPath, 'bin', executableName))) {
                return path.join(eqPath, 'bin', executableName);
            }
        }
    }

    return eqPath;
}

function isValidPythonPath(eqPath: string): boolean {
    return (
        fs.existsSync(eqPath) &&
        path.basename(getOSType() === OSType.Windows ? eqPath.toLowerCase() : eqPath).startsWith('eq')
    );
}
